package cn.iocoder.yudao.module.vos.service.ecdr.app;

import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.vos.controller.app.edcr.vo.*;
import cn.iocoder.yudao.module.vos.dal.dataobject.ecdr.ECdrDO;
import cn.iocoder.yudao.module.vos.dal.dataobject.secretaccount.SecretAccountDO;
import cn.iocoder.yudao.module.vos.dal.dataobject.secretaccountproductprice.SecretAccountProductPriceDO;
import cn.iocoder.yudao.module.vos.service.secretaccount.SecretAccountService;
import cn.iocoder.yudao.module.vos.service.secretaccountproductprice.SecretAccountProductPriceService;
import cn.iocoder.yudao.module.vos.utils.CdrUtil;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.model.Filters;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.bson.Document;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.*;

import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.vos.enums.ErrorCodeConstants.*;

@Service
@RequiredArgsConstructor
@Slf4j
public class CdrOpenApiServiceImpl implements CdrOpenApiService {

    @Resource
    private MongoTemplate mongoTemplate;

    @Resource
    private SecretAccountService secretAccountService;

    @Resource
    private SecretAccountProductPriceService secretAccountProductPriceService;

    /** v1 集合（原始 cdr）与 v2 集合（语义抽取 cdr_ai） */
    private static final String MONGO_COL    = "cdr";
    private static final String MONGO_COL_AI = "cdr_ai";

    /** 权限常量（同时也是全局定价表的 product_permission 主键） */
    private static final String PERM_SEARCH      = "risk_control_search_1";
    private static final String PERM_RETRO       = "risk_control_retrospective";
    private static final String PERM_SEARCH_V2   = "risk_control_search_2";

    /** 仍使用 CdrUtil.ZONE（马尼拉时区） */
    private static final DateTimeFormatter MANILA_FMT =
            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(CdrUtil.ZONE);

    // ===================================== v1：保持原逻辑 =====================================

    @Transactional(rollbackFor = Exception.class)
    @Override
    public CdrSearchRespVO searchCdr(String appSecret, CdrSearchReqVO reqVO, String ip) {
        SecretAccountDO account = secretAccountService.checkAndGetAccount(appSecret, PERM_SEARCH);

        String requestNo = System.currentTimeMillis() + "_" + UUID.randomUUID().toString().replace("-", "");
        String rawNumber = StrUtil.blankToDefault(reqVO.getNumber(), "").trim();
        if (StrUtil.isBlank(rawNumber)) throw exception(PARAMS_INCOMPLETE);

        String phone = CdrUtil.trimNumber(rawNumber);
        if (StrUtil.isBlank(phone)) throw exception(INVALID_PHONE_FORMAT);

        List<CdrItemRespVO> items = queryFromMongoOneYear(phone);

        SecretAccountProductPriceDO priceDO = secretAccountProductPriceService.getSecretAccountProductPrice(PERM_SEARCH);
        BigDecimal unitPrice = (priceDO != null && priceDO.getUnitPrice() != null) ? priceDO.getUnitPrice() : BigDecimal.ZERO;
        boolean shouldCharge = unitPrice.compareTo(BigDecimal.ZERO) > 0 && !items.isEmpty();

        String url = "/data/v1/search";
        String paramsMasked = "number=" + rawNumber + "&appSecret=***";
        String respBrief = "{size:" + items.size() + ", charged:" + (shouldCharge ? "Y" : "N") + "}";
        secretAccountService.chargeAndLog(account, unitPrice, shouldCharge, url, paramsMasked, PERM_SEARCH, respBrief, requestNo, ip);

        CdrSearchRespVO resp = new CdrSearchRespVO();
        resp.setRequestNumber(requestNo);
        resp.setData(items);
        return resp;
    }

    /** Mongo：返回该号码最近一年的所有记录（从 cdr 文档的 records.{yyyyMMdd} 数组中汇总）。 */
    private List<CdrItemRespVO> queryFromMongoOneYear(String phone) {
        LocalDate yesterday = LocalDate.now(CdrUtil.ZONE).minusDays(1);
        return queryFromMongoWindow(phone, yesterday, 365);
    }

    /** 在 [cutoff-(days-1), cutoff]（含端点）窗口内汇总号码的所有记录（集合 cdr）。 */
    private List<CdrItemRespVO> queryFromMongoWindow(String phone, LocalDate cutoff, int days) {
        MongoCollection<Document> col = mongoTemplate.getDb().getCollection(MONGO_COL);
        Document doc = col.find(Filters.eq("_id", phone)).first();
        if (doc == null) return Collections.emptyList();

        Object recObj = doc.get("records");
        if (!(recObj instanceof Document)) return Collections.emptyList();
        Document records = (Document) recObj;

        LocalDate start = cutoff.minusDays(Math.max(0, days - 1));
        List<CdrItemRespVO> list = new ArrayList<>(Math.min(days, 365) * 32);

        for (Map.Entry<String, Object> e : records.entrySet()) {
            String dayKey = e.getKey();
            if (!isYyyyMMdd(dayKey)) continue;

            LocalDate day = toLocalDate(dayKey);
            if (day.isBefore(start) || day.isAfter(cutoff)) continue;

            Object arr = e.getValue();
            if (!(arr instanceof List)) continue;

            @SuppressWarnings("unchecked")
            List<Document> calls = (List<Document>) arr;
            for (Document c : calls) {
                list.add(toItem(c)); // v1：扁平结构
            }
        }
        return list;
    }

    // v1: 扁平结构 -> CdrItemRespVO
    private CdrItemRespVO toItem(Document c) {
        String callergatewayid  = asStr(c.get("callergatewayid"));
        String calleeAccessE164 = asStr(c.get("calleeaccesse164"));

        String startStr = toManilaString(c.get("starttime"));
        Integer holdSec = safeInt(toLong(c.get("holdtime")));
        Integer method  = toInt(c.get("enddirection"));

        ECdrDO toCountType = new ECdrDO();
        toCountType.setCalleeaccesse164(calleeAccessE164);
        toCountType.setCallergatewayid(callergatewayid);
        String typeStr = CdrUtil.computePhoneTag(toCountType);
        int type = Integer.parseInt(typeStr);

        String source = obfuscateSource(callergatewayid);

        CdrItemRespVO vo = new CdrItemRespVO();
        vo.setSource(source);
        vo.setStarttime(startStr);
        vo.setMethod(method == null ? 0 : method);
        vo.setHoldtime(holdSec == null ? 0 : holdSec);
        vo.setType(type);
        return vo;
    }

    // ===================================== v2：来自 cdr_ai，返回 parsed =====================================

    /**
     * v2 查询：数据来自 cdr_ai，按号码近 365 天。
     * - 权限：risk_control_search_2
     * - 每条 item 使用 meta 组装基本字段（source 脱敏，type 固定为 0）
     * - transcript 不返回
     * - 返回 parsed：先取 company_name，递归把 parsed 中所有包含 company_name 的字符串（忽略大小写）替换为 obfuscateSource(company_name)
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public CdrSearchV2RespVO searchCdrV2(String appSecret, CdrSearchReqVO reqVO, String ip) {
        // 1) 权限校验
        SecretAccountDO account = secretAccountService.checkAndGetAccount(appSecret, PERM_SEARCH_V2);

        // 2) 参数清洗
        String requestNo = System.currentTimeMillis() + "_" + UUID.randomUUID().toString().replace("-", "");
        String rawNumber = StrUtil.blankToDefault(reqVO.getNumber(), "").trim();
        if (StrUtil.isBlank(rawNumber)) throw exception(PARAMS_INCOMPLETE);
        String phone = CdrUtil.trimNumber(rawNumber);
        if (StrUtil.isBlank(phone)) throw exception(INVALID_PHONE_FORMAT);

        // 3) 读取 cdr_ai（近 365 天）
        List<CdrItemV2RespVO> items = queryFromMongoAiOneYear(phone);

        // 4) 计费（risk_control_search_2）
        SecretAccountProductPriceDO priceDO = secretAccountProductPriceService.getSecretAccountProductPrice(PERM_SEARCH_V2);
        BigDecimal unitPrice = (priceDO != null && priceDO.getUnitPrice() != null) ? priceDO.getUnitPrice() : BigDecimal.ZERO;
        boolean shouldCharge = unitPrice.compareTo(BigDecimal.ZERO) > 0 && !items.isEmpty();

        String url = "/data/v2/search";
        String paramsMasked = "number=" + rawNumber + "&appSecret=***";
        String respBrief = "{size:" + items.size() + ", charged:" + (shouldCharge ? "Y" : "N") + "}";
        secretAccountService.chargeAndLog(account, unitPrice, shouldCharge, url, paramsMasked, PERM_SEARCH_V2, respBrief, requestNo, ip);

        // 5) 返回
        CdrSearchV2RespVO resp = new CdrSearchV2RespVO();
        resp.setRequestNumber(requestNo);
        // 这里直接塞 v2 的 VO 列表；如果 CdrSearchRespVO 的 data 使用了具体泛型，请相应调整其定义为通用 List<Object> 或新增一个 RespVO
        resp.setData(items);
        return resp;
    }

    private List<CdrItemV2RespVO> queryFromMongoAiOneYear(String phone) {
        LocalDate yesterday = LocalDate.now(CdrUtil.ZONE).minusDays(1);
        return queryFromMongoAiWindow(phone, yesterday, 365);
    }

    /** 读取 cdr_ai：records.{yyyyMMdd} -> 每条 item 内含 meta / transcript / parsed / parsed_raw */
    private List<CdrItemV2RespVO> queryFromMongoAiWindow(String phone, LocalDate cutoff, int days) {
        MongoCollection<Document> col = mongoTemplate.getDb().getCollection(MONGO_COL_AI);
        Document doc = col.find(Filters.eq("_id", phone)).first();
        if (doc == null) return Collections.emptyList();

        Object recObj = doc.get("records");
        if (!(recObj instanceof Document)) return Collections.emptyList();
        Document records = (Document) recObj;

        LocalDate start = cutoff.minusDays(Math.max(0, days - 1));
        List<CdrItemV2RespVO> list = new ArrayList<>(Math.min(days, 365) * 32);

        for (Map.Entry<String, Object> e : records.entrySet()) {
            String dayKey = e.getKey();
            if (!isYyyyMMdd(dayKey)) continue;

            LocalDate day = toLocalDate(dayKey);
            if (day.isBefore(start) || day.isAfter(cutoff)) continue;

            Object arr = e.getValue();
            if (!(arr instanceof List)) continue;

            @SuppressWarnings("unchecked")
            List<Document> calls = (List<Document>) arr;
            for (Document item : calls) {
                Document meta   = item.get("meta", Document.class);
                Document parsed = item.get("parsed", Document.class); // 可能为 null
                // 如果没有 parsed，看看是否有 parsed_raw（字符串），可以忽略或尝试 parse；这里忽略，返回 null
                if (parsed != null) {
                    String companyName = asStr(parsed.get("company_name")).trim();
                    if (!companyName.isEmpty()) {
                        String masked = obfuscateSource(companyName);
                        obfuscateCompanyEverywhere(parsed, companyName, masked); // 脱敏 parsed
                    }
                }

                CdrItemV2RespVO vo = toItemV2(meta, parsed);
                if (vo != null) list.add(vo);
            }
        }
        return list;
    }

    /** v2: 从 meta + parsed 转为 CdrItemV2RespVO；type 固定为 0；返回脱敏后的 parsed（Map 结构） */
    private CdrItemV2RespVO toItemV2(Document meta, Document parsed) {
        if (meta == null) return null;

        String callergatewayid = asStr(meta.get("callergatewayid"));

        // 优先使用 parsed.company_name 做 source 脱敏，否则 fallback 到 callergatewayid
        String companyName = (parsed == null) ? "" : asStr(parsed.get("company_name")).trim();
        String source = !companyName.isEmpty() ? obfuscateSource(companyName) : obfuscateSource(callergatewayid);

        String startStr = toManilaString(meta.get("starttime"));
        Integer holdSec = safeInt(toLong(meta.get("holdtime")));
        Integer method  = toInt(meta.get("enddirection"));

        CdrItemV2RespVO vo = new CdrItemV2RespVO();
        vo.setSource(source);
        vo.setStarttime(startStr);
        vo.setMethod(method == null ? 0 : method);
        vo.setHoldtime(holdSec == null ? 0 : holdSec);
        vo.setType(0); // 不使用原有类型计算

        // 返回 parsed：将 Document/List 递归转普通 Java Map/List，方便序列化
        vo.setParsed(parsed == null ? null : castBsonToPlain(parsed));
        return vo;
    }

    /** 递归把 parsed 内所有包含 companyName 的字符串用 masked 替换（忽略大小写） */
    @SuppressWarnings("unchecked")
    private void obfuscateCompanyEverywhere(Object node, String companyName, String masked) {
        if (node == null) return;
        if (node instanceof Document) {
            Document d = (Document) node;
            for (Map.Entry<String, Object> en : d.entrySet()) {
                Object v = en.getValue();
                if (v instanceof String) {
                    en.setValue(replaceIgnoreCase((String) v, companyName, masked));
                } else if (v instanceof Document || v instanceof List) {
                    obfuscateCompanyEverywhere(v, companyName, masked);
                }
            }
        } else if (node instanceof List) {
            List<Object> arr = (List<Object>) node;
            for (int i = 0; i < arr.size(); i++) {
                Object v = arr.get(i);
                if (v instanceof String) {
                    arr.set(i, replaceIgnoreCase((String) v, companyName, masked));
                } else if (v instanceof Document || v instanceof List) {
                    obfuscateCompanyEverywhere(v, companyName, masked);
                }
            }
        }
    }

    /** 忽略大小写替换所有出现的片段 */
    private String replaceIgnoreCase(String text, String search, String replacement) {
        if (text == null || search == null || search.isEmpty()) return text;
        return text.replaceAll("(?i)" + java.util.regex.Pattern.quote(search), replacement);
    }

    /** 将 BSON Document/List 递归转为普通 Java Map/List，便于返回给前端序列化 */
    @SuppressWarnings("unchecked")
    private Map<String, Object> castBsonToPlain(Document doc) {
        Map<String, Object> map = new LinkedHashMap<>();
        for (Map.Entry<String, Object> e : doc.entrySet()) {
            map.put(e.getKey(), bsonToPlain(e.getValue()));
        }
        return map;
    }

    @SuppressWarnings("unchecked")
    private Object bsonToPlain(Object val) {
        if (val == null) return null;
        if (val instanceof Document) {
            return castBsonToPlain((Document) val);
        } else if (val instanceof List) {
            List<Object> in = (List<Object>) val;
            List<Object> out = new ArrayList<>(in.size());
            for (Object o : in) out.add(bsonToPlain(o));
            return out;
        }
        return val;
    }

    // ===================================== 溯源逻辑（保持不变） =====================================

    @Transactional(rollbackFor = Exception.class)
    @Override
    public CdrSearchRespVO searchCdrRetrospective(String appSecret, CdrRetroReqVO reqVO, String ip) {
        SecretAccountDO account = secretAccountService.checkAndGetAccount(appSecret, PERM_RETRO);

        String requestNo = System.currentTimeMillis() + "_" + UUID.randomUUID().toString().replace("-", "");
        String rawNumber = StrUtil.blankToDefault(reqVO.getNumber(), "").trim();
        if (StrUtil.isBlank(rawNumber)) throw exception(PARAMS_INCOMPLETE);
        String phone = CdrUtil.trimNumber(rawNumber);
        if (StrUtil.isBlank(phone)) throw exception(INVALID_PHONE_FORMAT);

        String cutoffStr = StrUtil.blankToDefault(reqVO.getCutoffDate(), "").trim();
        if (StrUtil.isBlank(cutoffStr)) throw exception(CUTOFF_DATE_REQUIRED);
        LocalDate cutoff = parseToLocalDate(cutoffStr);

        int days = (reqVO.getCycleDays() == null) ? 365 : Math.max(1, Math.min(365, reqVO.getCycleDays()));

        List<CdrItemRespVO> items = queryFromMongoWindow(phone, cutoff, days);

        SecretAccountProductPriceDO priceDO = secretAccountProductPriceService.getSecretAccountProductPrice(PERM_RETRO);
        BigDecimal unitPrice = (priceDO != null && priceDO.getUnitPrice() != null) ? priceDO.getUnitPrice() : BigDecimal.ZERO;
        boolean shouldCharge = unitPrice.compareTo(BigDecimal.ZERO) > 0 && !items.isEmpty();

        String url = "/data/v1/retrospective";
        String paramsMasked = "number=" + rawNumber + "&cutoffDate=" + cutoffStr + "&days=" + days + "&appSecret=***";
        String respBrief = "{size:" + items.size() + ", charged:" + (shouldCharge ? "Y" : "N") + "}";
        secretAccountService.chargeAndLog(account, unitPrice, shouldCharge, url, paramsMasked, PERM_RETRO, respBrief, requestNo, ip);

        CdrSearchRespVO resp = new CdrSearchRespVO();
        resp.setRequestNumber(requestNo);
        resp.setData(items);
        return resp;
    }

    // ===================================== 公用装配/解析（与 v1 共享） =====================================

    private boolean isYyyyMMdd(String s) {
        return s != null && s.length() == 8 && s.chars().allMatch(Character::isDigit);
    }

    private LocalDate toLocalDate(String yyyyMMdd) {
        return LocalDate.of(
                Integer.parseInt(yyyyMMdd.substring(0, 4)),
                Integer.parseInt(yyyyMMdd.substring(4, 6)),
                Integer.parseInt(yyyyMMdd.substring(6, 8))
        );
    }

    private LocalDate parseToLocalDate(String input) {
        String s = input.trim();
        if (s.matches("^\\d{10,13}$")) {
            long n = Long.parseLong(s);
            if (s.length() == 13) n /= 1000;
            return Instant.ofEpochSecond(n).atZone(CdrUtil.ZONE).toLocalDate();
        }
        if (s.length() >= 10) {
            try {
                if (s.length() >= 19) s = s.substring(0, 19);
                if (s.length() == 10) {
                    return LocalDate.parse(s, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
                } else {
                    LocalDateTime ldt = LocalDateTime.parse(s, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
                    return ldt.toLocalDate();
                }
            } catch (Exception ignore) { }
        }
        throw exception(CUTOFF_DATE_FORMAT_INVALID);
    }

    private String toManilaString(Object v) {
        Long sec = toEpochSeconds(v);
        if (sec == null) return "-";
        return MANILA_FMT.format(Instant.ofEpochSecond(sec));
    }

    private Long toEpochSeconds(Object v) {
        if (v == null) return null;
        if (v instanceof Number) {
            long n = ((Number) v).longValue();
            return n < 1_000_000_000_000L ? n : n / 1000; // 10位秒 / 13位毫秒
        }
        String s = String.valueOf(v).trim();
        if (s.matches("^\\d{10,13}$")) {
            long n = Long.parseLong(s);
            return (s.length() == 10) ? n : n / 1000;
        }
        try {
            String norm = s.replace("T", " ");
            if (norm.length() >= 19) norm = norm.substring(0, 19);
            LocalDateTime ldt = LocalDateTime.parse(norm, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
            return ldt.toInstant(ZoneOffset.UTC).getEpochSecond();
        } catch (Exception ignore) { }
        return null;
    }

    private Integer safeInt(Long v) {
        return v == null ? null : (int) Math.max(Math.min(v, Integer.MAX_VALUE), 0);
    }

    private Long toLong(Object v) {
        if (v == null) return null;
        if (v instanceof Number) return ((Number) v).longValue();
        String s = String.valueOf(v).trim();
        if (s.matches("^\\d+$")) return Long.parseLong(s);
        return null;
    }

    private Integer toInt(Object v) {
        if (v == null) return null;
        if (v instanceof Number) return ((Number) v).intValue();
        String s = String.valueOf(v).trim();
        if (s.matches("^-?\\d+$")) return Integer.parseInt(s);
        return null;
    }

    /** 取 MD5 后的 [首、末、次首、次末、第三、倒数第三、第四、倒数第四] 大写拼接 */
    private String obfuscateSource(String input) {
        String src = StrUtil.blankToDefault(input, "");
        String md5 = md5Hex(src);
        if (md5.length() < 8) {
            return md5.toUpperCase(Locale.ROOT);
        }
        int n = md5.length();
        char[] cs = new char[]{
                md5.charAt(0), md5.charAt(n - 1),
                md5.charAt(1), md5.charAt(n - 2),
                md5.charAt(2), md5.charAt(n - 3),
                md5.charAt(3), md5.charAt(n - 4)
        };
        return new String(cs).toUpperCase(Locale.ROOT);
    }

    private String md5Hex(String s) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] out = md.digest(s.getBytes(StandardCharsets.UTF_8));
            StringBuilder sb = new StringBuilder(out.length * 2);
            for (byte b : out) {
                String h = Integer.toHexString(b & 0xff);
                if (h.length() == 1) sb.append('0');
                sb.append(h);
            }
            return sb.toString();
        } catch (Exception e) {
            return "000000";
        }
    }

    private String asStr(Object v) {
        return v == null ? "" : String.valueOf(v);
    }
}
